先推荐一篇Java注释的初级文章。《Tiger 中的注释,第 1 部分: 向 Java 代码中添加元数据》。
Java的注释功能一直在用,最常用的就是@Override,以及在泛型里常见的@SuppressWarnings。这是我们对Java注释的第一印象。其实它们是Java仅有的3个內建 “标准注释“ 之一,他们分别是:
public class OverrideTester {
@Override
public String toString() {
return super.toString() + " [Override Tester Implementation]";
}
}
@SuppressWarings(value={"unchecked"})
public void nonGenericsMethod() {
List wordList = new ArrayList(); // no typing information on the List
wordList.add("foo"); // causes error on list addition
}
public class DeprecatedClass {
@Deprecated public void doSomething() {
// some code
}
}
这篇文章是上一篇的下半部分。《Tiger 中的注释,第 2 部分: 定制注释》
除了3个“标准注释”,Java还为用户提供了扩展”自定义注释“的途径。这需要用到4个內建“元注释”。
下面这个例子,演示了怎么定义一个最简单的注释:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}
首先用@interface来声明一个注释。我们的新注释叫UseCase。
然后Target是ElementType.METHOD,说明这个注释只能用来修饰方法。 Retention是RetentionPolicy.RUNTIME。表明就算在程序运行的时候,我们都能从字节码中找到这个注释。
最后定义注释属性的语法有点怪:用声明方法的形式来声明注释的属性。返回值则代表属性数据类型。后面的特殊语法“default”用来给定属性默认值。
最后的使用场景就会是像下面这样:
public class PasswordUtils {
@UseCase(id = 47, description = "Passwords must contain at least one numeric")
public boolean validatePassword(String password) {
return (password.matches("\\w*\\d\\w*"));
}
@UseCase(id = 48)
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "New passwords can’t equal previously used ones")
public boolean checkForNewPassword(
List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
三个用注释标记出来的方法,分别对应了最初设计的用例。
“反射”能够在运行时获取类型信息。如果注释的保留等级是@RetentionPolicy.RUNTIME的话,反射是能够获取并识别的。
Java的java.lang.reflect包里的“getAnnotation()”方法可以识别出标记的注释。还是刚才UseCase的例子,下面这个类可以自动读取注释信息。
public class UseCaseTracker {
public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("Found Use Case:" + uc.id() + " " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for(int i : useCases) {
System.out.println("Warning: Missing use case-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
思路很简单,用getDeclaredMethods()方法从Class对象获得所有定义的方法,然后用getAnnotation()读取注释。最后的效果会像下面这样。这个功能对产品经理控制项目进度来说很实用。能直观显示产品用例的完成进度。
Found Use Case:47 Passwords must contain at least one numeric
Found Use Case:48 no description
Found Use Case:49 New passwords can’t equal previously used ones
Warning: Missing use case-50
这里必须推荐知乎上曹旭东的一个精彩回答:《java注释是怎么实现的?》。
简单地说就是:
熟悉动态代理的话,就知道编译器通过我们贴标记时提供的信息自动生成了一个代理类的字节码,然后又生成一个实例。所以标记的处理,也是一个深入到字节码范畴的事情。
具体的探索过程和代码,可以去看具体的回答。至少现在我们就可以大概知道注释是以怎样的身份存在在字节码中。以及反射是怎么在运行时获取这个标记了。
从刚才UseCase的例子,程序员大概就能知道Java注释的潜力了。现代Java编程的前沿技术里,注释这个“小功能”已经在数据端访问领域大放异彩。最著名的例子就是三大框架之一的Hibernate利用注释为类自动生成后端数据库表以及各种SQL操作语句。练习题1就是一个很好的例子。这里就不重复贴代码了。
注释另一个大展身手的地方,是在自动单元测试领域。书中介绍的Unit框架并没有得到推广。实际应用最多的当然是“JUnit单元测试框架”.
推荐一篇最简单的入门文档 – 《Getting started》。告诉我们JUnit利用注释做单元测试的原理。
一个最简单的例子是这样:假设我们有一个计算器。单元测试需要验证计算器的计算结果是否正确。
public class Calculator {
public int evaluate(String expression) {
int sum = 0;
for (String summand: expression.split("\\+"))
sum += Integer.valueOf(summand);
return sum;
}
}
我们给计算器写了一个测试类。然后用最简单的“@Test”把测试方法标记上。
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class CalculatorTest {
@Test
public void evaluatesExpression() {
Calculator calculator = new Calculator();
int sum = calculator.evaluate("1+2+3");
assertEquals(6, sum);
}
}
然后编译,运行我们的测试类就好了。编译运行的时候都要带上JUnit的类库。
javac -cp .:junit-4.XX.jar CalculatorTest.java
java -cp .:junit-4.XX.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore CalculatorTest
下面这篇文章,介绍地更加深入一些。《单元测试利器 JUnit 4》。提到了JUnit一些最常用的模块和功能。比如说:
测试经常用到很多公共资源或者数据,例如测试环境,测试数据。Fixture功能允许共用这些资源,不必每次测试都重新加载。
允许自定义测试运行器。其实测试运行器其实就是之前我们说的测试处理器。
“测试套件”功能允许批量处理测试。
除了“测试套件”,JUnit还允许对同一个测试类配置不同的参数进行批量测试。
JUnit自动单元测试,源自“敏捷编程”,“极限编程”和“测试驱动开发”这样的理念。非常地“政治正确”。
但很多公司也反映JUnit在中型或者大型项目中,随着代码规模的扩张,JUnit代码开始变得有点“难维护”。
由注释驱动的单元测试的模式的缺点正在慢慢浮现。
注释的威力还远不止Hibernate和JUnit。著名的面向切面编程(AOP)的理念也是动态代理和注释的衍生技术。火热的Spring框架就是一杆这样的一杆大旗。
基本原理和之前的SQL语句自动生成,还有自动测试代码生成类似。细节就不展开了,参见后面的两篇文章: 《Aspect-Oriented Programming(AOP)面向切面编程》 《Aspect-Oriented Programming(AOP)面向切面编程 (续)》
很不幸,书中重点介绍的APT工具在Java 7被弃用。
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.TYPE) // Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNull() default true;
boolean unique() default false;
}
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLDecimal{
public int value() default 0;
public String name() default "";
public Constraints constraints() default @Constraints;
}
package com.ciaoshen.thinkinjava.chapter20.db;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLDate{
public int value() default 0;
public String name() default "";
public Constraints constraints() default @Constraints;
}
package com.ciaoshen.thinkinjava.chapter20.db;
@DBTable(name = "MEMBER")
public class Member {
@SQLString(30) String firstName;
@SQLString(50) String lastName;
@SQLInteger Integer age;
@SQLDecimal(10) Integer height;
@SQLDate(20) Long birthday;
@SQLString(value = 30, constraints = @Constraints(primaryKey = true)) String handle;
static int memberCount;
public String getHandle() { return handle; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String toString() { return handle; }
public Integer getAge() { return age; }
}
package com.ciaoshen.thinkinjava.chapter20;
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
import com.ciaoshen.thinkinjava.chapter20.db.*;
public class Exercise1 {
public static void main(String[] args) throws Exception {
if(args.length < 1) {
System.out.println("arguments: annotated classes");
System.exit(0);
}
for(String className : args) {
Class< ? > cl = Class.forName(className);
/**
* Table Name
*/
DBTable dbTable = cl.getAnnotation(DBTable.class);
if(dbTable == null) {
System.out.println("No DBTable annotations in class " + className);
continue;
}
String tableName = dbTable.name();
// If the name is empty, use the Class name:
if(tableName.length() < 1){
tableName = cl.getName().toUpperCase();
}
/**
* SQL
*/
List<String> columnDefs = new ArrayList<String>();
for(Field field : cl.getDeclaredFields()) {
String columnName = null;
Annotation[] anns = field.getDeclaredAnnotations();
if(anns.length < 1){
continue; // Not a db table column
}
if(anns[0] instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) anns[0];
// Use field name if name not specified
if(sInt.name().length() < 1){
columnName = field.getName().toUpperCase();
}else{
columnName = sInt.name();
}
columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));
}
if(anns[0] instanceof SQLString) {
SQLString sString = (SQLString) anns[0];
// Use field name if name not specified.
if(sString.name().length() < 1){
columnName = field.getName().toUpperCase();
}else{
columnName = sString.name();
}
columnDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + getConstraints(sString.constraints()));
}
if(anns[0] instanceof SQLDecimal) {
SQLDecimal sd = (SQLDecimal) anns[0];
// Use field name if name not specified.
if(sd.name().length() < 1){
columnName = field.getName().toUpperCase();
}else{
columnName = sd.name();
}
columnDefs.add(columnName + " DECIMAL(" + sd.value() + ")" + getConstraints(sd.constraints()));
}
if(anns[0] instanceof SQLDate) {
SQLDate sd = (SQLDate) anns[0];
// Use field name if name not specified.
if(sd.name().length() < 1){
columnName = field.getName().toUpperCase();
}else{
columnName = sd.name();
}
columnDefs.add(columnName + " DATE(" + sd.value() + ")" + getConstraints(sd.constraints()));
}
StringBuilder createCommand = new StringBuilder("CREATE TABLE " + tableName + "(");
for(String columnDef : columnDefs){
createCommand.append("\n " + columnDef + ",");
}
// Remove trailing comma
String tableCreate = createCommand.substring(0, createCommand.length() - 1) + ");";
System.out.println("Table Creation SQL for " + className + " is :\n" + tableCreate);
}
}
}
private static String getConstraints(Constraints con) {
String constraints = "";
if(!con.allowNull()){
constraints += " NOT NULL";
}
if(con.primaryKey()){
constraints += " PRIMARY KEY";
}
if(con.unique()){
constraints += " UNIQUE";
}
return constraints;
}
}